﻿using System;
using System.Collections.Generic;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Threading;
using System.Threading.Tasks;
using Windows.Foundation;
using Windows.Storage;
using Windows.Storage.Streams;
using Windows.Web.Http;
using Windows.Web.Http.Headers;

namespace MullenStudio.Storage
{
    /// <summary>
    /// A IRandomAccessStream than can be read while downloading
    /// </summary>
    public class DownloadRandomAccessStream : IRandomAccessStream
    {
        #region IRandomAccessStream Implementation
        #region Properties
        public bool CanRead
        {
            get { return true; }
        }

        public bool CanWrite
        {
            get { return false; }
        }

        ulong _position = 0;
        public ulong Position
        {
            get { return _position; }
        }

        public ulong _size;
        public ulong Size
        {
            get { return _size; }
            set { throw new NotSupportedException(); }
        }
        #endregion

        #region Not Supported Methods
        public IRandomAccessStream CloneStream()
        {
            throw new NotSupportedException();
        }

        public IAsyncOperation<bool> FlushAsync()
        {
            throw new NotSupportedException();
        }

        public IAsyncOperationWithProgress<uint, uint> WriteAsync(IBuffer buffer)
        {
            throw new NotSupportedException();
        }
        #endregion

        #region DownloadInputStream
        /// <summary>
        /// IInputStream for DownloadRandomAccessStream
        /// </summary>
        public class DownloadInputStream : IInputStream
        {
            ulong _position;
            IInputStream _inputStream;
            internal DownloadInputStream(DownloadRandomAccessStream parent, ulong position)
            {
                _parent = parent;
                _position = position;
                _inputStream = _parent._stream.GetInputStreamAt(_position);
            }

            DownloadRandomAccessStream _parent;

            public IAsyncOperationWithProgress<IBuffer, uint> ReadAsync(IBuffer buffer, uint count, InputStreamOptions options)
            {
                return AsyncInfo.Run(delegate(CancellationToken cancellationToken, IProgress<uint> progress)
                {
                    return ReadAsyncInternal(buffer, count, options, cancellationToken, progress);
                });
            }

            protected async Task<IBuffer> ReadAsyncInternal(IBuffer buffer, uint count, InputStreamOptions options, CancellationToken cancellationToken, IProgress<uint> progress)
            {
                progress.Report(0);

                if (count > _parent._size - _position)
                    count = (uint)(_parent._size - _position);

                //_parent.Log("<<Reading from {0} to {1}", _position, _position + count);

                _parent.Seek(_position);

                _parent._currentReadingPosition = _position;

                do
                {
                    bool prepared = false;
                    foreach (DataBlock block in _parent._availableBlocks)
                    {
                        if (block.IsInside(_position, true))
                        {
                            if (_position + count <= block.End)
                                prepared = true;
                            else
                                progress.Report((uint)(block.End - _position));
                            break;
                        }
                    }
                    if (prepared)
                        break;
                    else
                    {
                        await Task.Delay(100, cancellationToken);
                        cancellationToken.ThrowIfCancellationRequested();
                    }
                } while (true);

                _parent._semaphore.WaitOne();
                try
                {
                    IBuffer result = await _inputStream.ReadAsync(buffer, count, options).AsTask(cancellationToken);
                    //_parent.Log("<<Read from {0} to {1}", _position, _position + result.Length);
                    _position += result.Length;
                    progress.Report(count);
                    return result;
                }
                finally
                {
                    _parent._semaphore.Release();
                }
            }

            public void Dispose()
            {
                _inputStream.Dispose();
            }
        }
        #endregion

        public IInputStream GetInputStreamAt(ulong position)
        {
            if (!IsInitialized)
                throw new InvalidOperationException();
            return new DownloadInputStream(this, position);
        }

        public IOutputStream GetOutputStreamAt(ulong position)
        {
            throw new NotSupportedException();
        }

        public void Seek(ulong position)
        {
            if (!IsInitialized)
                throw new InvalidOperationException();

            // If Seek and ReadAsync are called sequentially, as ReadAsync will call Seek again, Seek function will be called twice
            // If the first Seek is not finished when the second Seek starts, the second Seek may start a new ReceiveAsyncInternal task.
            // Therefore, need to ignore the second Seek call.
            if (_isSeeking)
                return;

            _isSeeking = true;

            if (_position < 0)
                _position = 0;
            else if (_position > _size)
                _position = _size;
            else
                _position = position;
            //Log("------Seek({0})------", position);
            _semaphore.WaitOne();
            ulong newPos = ulong.MaxValue;  // New position to start download, set it only when associated block is created
            try
            {
                // Find if there is any block already contain the position
                foreach (DataBlock block in _availableBlocks)
                {
                    if (block.IsInside(_position, true))
                    {
                        if (block == _currentBlock && IsDownloading)
                        {
                            // The block is current block, and is downloading now
                            // Just keep current downloading
                            _isSeeking = false;
                            return;
                        }
                        else if (block.End == _size)
                        {
                            // The block already meets the end of the file
                            // Do nothing, already got all data
                            _isSeeking = false;
                            return;
                        }
                        else
                        {
                            // Exsit block, but need to continue downloading from this block
                            // Start new downloading
                            newPos = block.End;
                            _currentBlock = block;
                            break;
                        }
                    }
                }
                if (newPos == ulong.MaxValue)
                {
                    // Need to download, but doesn't have an associated block
                    _currentBlock = new DataBlock(_position, _position);
                    newPos = _position;
                    // Insert new block to the list in order
                    bool inserted = false;
                    for (int i = 0; i < _availableBlocks.Count; i++)
                    {
                        if (_currentBlock.Start < _availableBlocks[i].Start)
                        {
                            _availableBlocks.Insert(i, _currentBlock);
                            inserted = true;
                            break;
                        }
                    }
                    if (!inserted)
                        _availableBlocks.Add(_currentBlock);
                    //LogAllBlocks();
                }

                if (_cts != null)
                {
                    _cts.Cancel();
                    _cts.Dispose();
                }

                _cts = new CancellationTokenSource();
            }
            finally
            {
                if (newPos == ulong.MaxValue)
                    _isSeeking = false;
                _semaphore.Release();
            }
            Task task = SeekAsyncInternal(newPos, _cts.Token).ContinueWith(_ =>
                ReceiveAsyncInternal(newPos, _cts.Token));
        }

        public void Dispose()
        {
            _stream.Dispose();
        }

        public IAsyncOperationWithProgress<IBuffer, uint> ReadAsync(IBuffer buffer, uint count, InputStreamOptions options)
        {
            if (!IsInitialized)
                throw new InvalidOperationException();
            IInputStream inputStream = GetInputStreamAt(_position);
            return inputStream.ReadAsync(buffer, count, options);
        }
        #endregion

        #region Constructor
        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="uri">Source URI</param>
        public DownloadRandomAccessStream(Uri uri)
        {
            SourceUri = new Uri(uri.OriginalString);

            BufferCapacity = 0x10000;
        }
        #endregion

        #region Private Fields
        InMemoryRandomAccessStream _stream;
        CancellationTokenSource _cts;
        Semaphore _semaphore = new Semaphore(1, 1);
        bool _isSeeking = false;
        ulong _currentReadingPosition;
        #endregion

        #region Data Block
        /// <summary>
        /// Information of a data block
        /// </summary>
        protected class DataBlock
        {
            /// <summary>
            /// Inclusive start position
            /// </summary>
            public ulong Start { get; set; }
            /// <summary>
            /// Exclusive end position
            /// </summary>
            public ulong End { get; set; }

            /// <summary>
            /// Constructor
            /// </summary>
            /// <param name="start">Start position</param>
            /// <param name="end">End position</param>
            public DataBlock(ulong start, ulong end)
            {
                Start = start;
                End = end;
            }

            /// <summary>
            /// Check if the given position is inside current block
            /// </summary>
            /// <param name="position">The given position</param>
            /// <param name="inclusive">If this is true, position == End will be considered as inside</param>
            /// <returns>True if inside</returns>
            public bool IsInside(ulong position, bool inclusive)
            {
                return position >= Start && (inclusive ? position <= End : position < End);
            }
        }

        List<DataBlock> _availableBlocks = new List<DataBlock>();
        DataBlock _currentBlock;
        #endregion

        #region Download Related Methoes
        /// <summary>
        /// Stop current download
        /// </summary>
        public void Pause()
        {
            // Start a new Task, to avoid current thread being blocked
            // E.g., if Pause is called inside an event, _semaphore will wait forever
            Task.Factory.StartNew(() =>
            {
                _semaphore.WaitOne();
                try
                {
                    if (_cts != null)
                    {
                        _cts.Cancel();
                        _cts.Dispose();
                    }
                    _cts = new CancellationTokenSource();
                    _isSeeking = false;
                }
                finally
                {
                    _semaphore.Release();
                }
            });
        }

        /// <summary>
        /// Initilize the stream
        /// </summary>
        /// <returns>Task</returns>
        /// <remarks>Must be called before other operations</remarks>
        public async Task InitializeAsync()
        {
            _semaphore.WaitOne();
            try
            {
                HttpClient httpClient = new HttpClient();

                HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, SourceUri);

                // Do not buffer the response. 
                _response = await httpClient.SendRequestAsync(request, HttpCompletionOption.ResponseHeadersRead);

                if (!_response.Content.Headers.ContentLength.HasValue)
                    throw new Exception();

                _size = _response.Content.Headers.ContentLength.Value;
                _stream = new InMemoryRandomAccessStream();
                _stream.Size = _size;
                _availableBlocks.Clear();
                _availableBlocks.Add(new DataBlock(0, 0));
                _currentBlock = _availableBlocks[0];
                //Log("Size:{0}", _size);

                BytesReceived = 0;
                TotalBytesToReceive = _size;

                IsInitialized = true;
            }
            finally
            {
                _semaphore.Release();
            }
        }

        private async Task SeekAsyncInternal(ulong position, CancellationToken cancellationToken)
        {
            _semaphore.WaitOne();
            try
            {
                HttpClient httpClient = new HttpClient();

                HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, SourceUri);
                request.Headers.Add(new KeyValuePair<string, string>("Range", string.Format("bytes={0}-", position)));

                _response = await httpClient.SendRequestAsync(request,
                    HttpCompletionOption.ResponseHeadersRead).AsTask(cancellationToken);
            }
            finally
            {
                _semaphore.Release();
            }
        }

        HttpResponseMessage _response;

        private async Task ReceiveAsyncInternal(ulong position, CancellationToken cancellationToken)
        {
            IsDownloading = true;
            _isSeeking = false;
            IInputStream responseStream = null;
            IOutputStream outputStream = _stream.GetOutputStreamAt(position);
            try
            {
                responseStream = await _response.Content.ReadAsInputStreamAsync();
                Windows.Storage.Streams.Buffer responseBuffer = new Windows.Storage.Streams.Buffer(BufferCapacity);
                while (true)
                {
                    uint toread = responseBuffer.Capacity;
                    int currentIdx = _availableBlocks.IndexOf(_currentBlock);   // Current block's index
                    if (currentIdx == _availableBlocks.Count - 1)
                    {
                        if (_size - _currentBlock.End < toread)
                            toread = (uint)(_size - _currentBlock.End);
                    }
                    else
                    {
                        System.Diagnostics.Debug.Assert(_availableBlocks[currentIdx + 1].Start > _currentBlock.End);
                        if (_availableBlocks[currentIdx + 1].Start - _currentBlock.End < toread)
                            toread = (uint)(_availableBlocks[currentIdx + 1].Start - _currentBlock.End);
                    }

                    if (toread > 0)
                        await responseStream.ReadAsync(responseBuffer, toread, InputStreamOptions.None);
                    else
                        responseBuffer.Length = 0;
                    uint read = responseBuffer.Length;

                    _semaphore.WaitOne();
                    try
                    {
                        cancellationToken.ThrowIfCancellationRequested();
                        ulong newPos = ulong.MaxValue;  // Set this value only if need to start downloading from new position
                        await outputStream.WriteAsync(responseBuffer);
                        _currentBlock.End += (ulong)read;
                        BytesReceived += (ulong)read;
                        if (read > 0)
                        {
                            if (DataReceived != null)
                                DataReceived(this, EventArgs.Empty);
                        }
                        //Log(">>Current Block: {0}-{1}", _currentBlock.Start, _currentBlock.End);
                        if (currentIdx < _availableBlocks.Count - 1)
                        {
                            // Has other blocks next to this one
                            System.Diagnostics.Debug.Assert(_currentBlock.End <= _availableBlocks[currentIdx + 1].Start);
                            if (_currentBlock.End == _availableBlocks[currentIdx + 1].Start)
                            {
                                // Combine these two blocks
                                _currentBlock.End = _availableBlocks[currentIdx + 1].End;
                                newPos = _currentBlock.End;
                                _availableBlocks.RemoveAt(currentIdx + 1);
                                //LogAllBlocks();
                            }
                        }
                        if (currentIdx == _availableBlocks.Count - 1)
                        {
                            // Working on the last available block
                            if (_currentBlock.End == _size)
                            {
                                // Meet the end of the file
                                if (AutoFillBlank && currentIdx > 0)
                                {
                                    // Still has some parts to fill, and the policy is to fill them
                                    // Go back from the first block
                                    _currentBlock = _availableBlocks[0];
                                    newPos = _currentBlock.End;
                                }
                                else
                                {
                                    // Either completed, all just finished to the end of the file and th policy doesn't require to go back
                                    // Finish Downloading
                                    if (DownloadFinished != null)
                                        DownloadFinished(this, EventArgs.Empty);
                                    if (currentIdx == 0)
                                    {
                                        if (DownloadCompleted != null)
                                            DownloadCompleted(this, EventArgs.Empty);
                                    }
                                    return;
                                }
                            }
                        }

                        if (newPos < ulong.MaxValue)
                        {
                            // Start new downloading
                            responseStream.Dispose();
                            _semaphore.Release();
                            cancellationToken.ThrowIfCancellationRequested();
                            await SeekAsyncInternal(newPos, cancellationToken);
                            _semaphore.WaitOne();
                            responseStream = await _response.Content.ReadAsInputStreamAsync();
                            outputStream.Dispose();
                            outputStream = _stream.GetOutputStreamAt(newPos);
                        }

                        cancellationToken.ThrowIfCancellationRequested();
                    }
                    finally
                    {
                        _semaphore.Release();
                    }
                }
            }
            finally
            {
                outputStream.Dispose();
                if (responseStream != null)
                    responseStream.Dispose();
                IsDownloading = false;
            }
        }
        #endregion

        #region Download Related Properties
        /// <summary>
        /// Get whether the stream is already initialized
        /// </summary>
        /// <remarks>Must initialize before using it.</remarks>
        public bool IsInitialized { get; protected set; }
        /// <summary>
        /// Get or set the size of the download buffer
        /// </summary>
        public uint BufferCapacity { get; set; }
        /// <summary>
        /// Get whether the stream is currently downloading
        /// </summary>
        public bool IsDownloading { get; protected set; }
        /// <summary>
        /// Get or set if the download will automatically try to fill the blank block after meet the end of the file
        /// </summary>
        /// <remarks>
        /// For example, if the file size is 10000 and currently downloaded the blocks 1-2000 and is downloading the block 8000-,
        /// when 8000-10000 block finished, if this property is true, this stream will start downloading from 2000-;
        /// otherwise, it will stop downloading.
        /// </remarks>
        public bool AutoFillBlank { get; set; }
        /// <summary>
        /// Get the source Uri set by constructor
        /// </summary>
        public Uri SourceUri { get; protected set; }
        /// <summary>
        /// Get the headers of HttpResponse.Content
        /// </summary>
        public HttpContentHeaderCollection Headers
        {
            get
            {
                if (_response != null)
                    return _response.Content.Headers;
                else
                    return null;
            }
        }
        /// <summary>
        /// Get the number of bytes received so far
        /// </summary>
        public ulong BytesReceived { get; protected set; }
        /// <summary>
        /// Get the total number of bytes
        /// </summary>
        /// <remarks>Same as Size</remarks>
        public ulong TotalBytesToReceive { get; protected set; }
        /// <summary>
        /// Get the end position of the block that is curretly reading or just read
        /// </summary>
        /// <remarks>Usually can be used to indicate download progress for media player</remarks>
        public ulong CurrentReadingBlockEndPosition
        {
            get
            {
                foreach (DataBlock block in _availableBlocks)
                {
                    if (block.IsInside(_currentReadingPosition, true))
                        return block.End;
                }
                return _currentReadingPosition;
            }
        }
        /// <summary>
        /// Get the end position of first block
        /// </summary>
        /// <remarks>0-FirstBlockEndPosition is downloaded block</remarks>
        public ulong FirstBlockEndPosition
        {
            get { return _availableBlocks[0].End; }
        }
        #endregion

        #region Download Related Events
        public event EventHandler<EventArgs> DataReceived;
        public event EventHandler<EventArgs> DownloadCompleted;
        public event EventHandler<EventArgs> DownloadFinished;
        #endregion

        #region Helper Methods
        /// <summary>
        /// Print current block informations
        /// </summary>
        private void LogAllBlocks()
        {
            Log("******");
            foreach (DataBlock block in _availableBlocks)
            {
                if (block != _currentBlock)
                    Log("  {0}-{1}", block.Start, block.End);
                else
                    Log(" *{0}-{1}", block.Start, block.End);
            }
        }

        /// <summary>
        /// Pring log information for debug
        /// </summary>
        /// <param name="format">Format, same as WriteLine</param>
        /// <param name="args">Arguments</param>
        protected void Log(string format, params object[] args)
        {
            System.Diagnostics.Debug.WriteLine(string.Format(format, args));
        }

        /// <summary>
        /// Save current downloaded data to a file
        /// </summary>
        /// <param name="file">File</param>
        /// <returns>Task</returns>
        /// <remarks>If the download is not completed, the file will be not completed.</remarks>
        public async Task SaveToFileAsync(StorageFile file)
        {
            if (!IsInitialized)
                throw new InvalidOperationException();
            IRandomAccessStream fileStream = await file.OpenAsync(FileAccessMode.ReadWrite);
            Windows.Storage.Streams.Buffer dataBuffer = new Windows.Storage.Streams.Buffer(BufferCapacity);
            IInputStream inputStream = _stream.GetInputStreamAt(0);
            while (true)
            {
                uint read = (await inputStream.ReadAsync(dataBuffer, BufferCapacity, InputStreamOptions.None)).Length;
                if (read == 0)
                    break;
                await fileStream.WriteAsync(dataBuffer);
            }
        }
        #endregion
    }
}
